package com.stone.starter.redis.config;

import com.stone.starter.redis.client.RedisCacheClient;
import com.stone.starter.redis.client.RedisShardingClient;
import com.stone.starter.redis.config.properties.RedisCacheProperties;
import com.stone.starter.redis.config.properties.RedisShardingProperties;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;

import java.time.Duration;
import java.util.Map;
import java.util.Set;

/**
 * Redis连接池自动配置
 *
 * @author Mr_wenpan@163.com 2021-08-10
 */
@Configuration
@EnableConfigurationProperties({RedisShardingProperties.class, RedisCacheProperties.class})
public class StoneRedisAutoConfiguration {

    @ConditionalOnProperty(
            prefix = "stone.redis.sharding",
            name = {"enabled"},
            havingValue = "true"
    )
    @Bean
    public RedisShardingClient redisShardingClientsFactory(RedisShardingProperties redisShardingProperties) {
        // 创建一个RedisShardingClient对象，此时并没有连接到任何的Redis
        RedisShardingClient redisShardingClient = new RedisShardingClient();
        // 如果没有获取到配置的Redis实例信息，则注入一个空的redisShardingClient，该redisShardingClient不能对Redis进行操作
        if (redisShardingProperties.getInstances() == null || redisShardingProperties.getInstances().size() < 1) {
            return redisShardingClient;
        }
        // 获取并遍历配置的Redis实例信息
        for (Map.Entry<String, RedisShardingProperties.RedisConnector> varProperties : redisShardingProperties.getInstances().entrySet()) {
            // 获取配置的实例信息
            RedisShardingProperties.RedisConnector instanceProperties = varProperties.getValue();
            // 构建lettuce连接配置信息（用于构建lettuce连接工厂）
            LettuceClientConfiguration lettuceClientConfiguration = buildLettuceClientConfig(
                    redisShardingProperties.getPoolMinIdle(),
                    redisShardingProperties.getPoolMaxIdle(),
                    redisShardingProperties.getPoolMaxWaitMillis(),
                    instanceProperties.getTimeoutMillis());
            LettuceConnectionFactory lettuceConnectionFactory;
            // 如果application.yml配置信息中没有指定master节点，或者没有指定节点信息，则创建单机版的Redis连接配置
            if (StringUtils.isEmpty(instanceProperties.getMaster()) || StringUtils.isEmpty(instanceProperties.getNodes())) {
                RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
                redisStandaloneConfiguration.setHostName(instanceProperties.getHost());
                redisStandaloneConfiguration.setPort(instanceProperties.getPort());
                redisStandaloneConfiguration.setPassword(RedisPassword.of(instanceProperties.getPassword()));
                redisStandaloneConfiguration.setDatabase(instanceProperties.getDbIndex());
                // 通过Redis连接配置和lettuce客户端连接配置创建lettuce连接工厂
                lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration, lettuceClientConfiguration);
            } else {
                // 按逗号拆分配置的节点（哨兵节点）
                Set<String> sentinels = StringUtils.commaDelimitedListToSet(instanceProperties.getNodes());
                // 创建哨兵配置信息
                RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration(instanceProperties.getMaster(), sentinels);
                // 通过哨兵配置信息和lettuce客户端连接配置创建lettuce连接工厂
                lettuceConnectionFactory = new LettuceConnectionFactory(sentinelConfiguration, lettuceClientConfiguration);
            }
            // 手动调用afterPropertiesSet进行一些初始化工作
            lettuceConnectionFactory.afterPropertiesSet();
            // 将lettuce连接工厂保存起来（key就是application.yml中配置的实例名称）
            redisShardingClient.addRedisConnectionFactories(varProperties.getKey(), lettuceConnectionFactory);

            // 通过lettuce工厂创建redisTemplate用于操作Redis（此处的注入参考springboot自动配置中对StringRedisTemplate的注入）
            StringRedisTemplate redisTemplate = new StringRedisTemplate(lettuceConnectionFactory);
            // 将redisTemplate保存起来，方便后面动态取用
            redisShardingClient.addRedisTemplates(varProperties.getKey(), redisTemplate);
        }
        return redisShardingClient;
    }

    @Bean
    public RedisCacheClient cacheRedisTemplate(RedisCacheProperties redisCacheProperties) {
        // 构建lettuce客户端连接配置（用于创建连接工厂）
        LettuceClientConfiguration lettuceClientConfiguration = buildLettuceClientConfig(
                redisCacheProperties.getPoolMinIdle(),
                redisCacheProperties.getPoolMaxIdle(),
                redisCacheProperties.getPoolMaxWaitMillis(),
                redisCacheProperties.getTimeoutMillis());
        LettuceConnectionFactory lettuceConnectionFactory;
        // ========================================================================================================
        // 这里默认可以适配三种Redis部署方式：单机模式、集群模式、哨兵模式
        // 如果application.yml配置文件中配有配置主节点或没有节点信息，则默认是单机版
        // 如果不是集群模式，则认为是哨兵模式，主从模式没有
        // ========================================================================================================
        // 单机部署方式
        if (StringUtils.isEmpty(redisCacheProperties.getMaster()) || StringUtils.isEmpty(redisCacheProperties.getNodes())) {
            RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
            redisStandaloneConfiguration.setHostName(redisCacheProperties.getHost());
            redisStandaloneConfiguration.setPassword(RedisPassword.of(redisCacheProperties.getPassword()));
            redisStandaloneConfiguration.setPort(redisCacheProperties.getPort());
            redisStandaloneConfiguration.setDatabase(redisCacheProperties.getDbIndex());
            lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration, lettuceClientConfiguration);
        } else if (!redisCacheProperties.getClusterMode()) {
            // 哨兵部署方式
            Set<String> sentinels = StringUtils.commaDelimitedListToSet(redisCacheProperties.getNodes());
            RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration(redisCacheProperties.getMaster(), sentinels);
            sentinelConfiguration.setDatabase(redisCacheProperties.getDbIndex());
            lettuceConnectionFactory = new LettuceConnectionFactory(sentinelConfiguration, lettuceClientConfiguration);
        } else {
            // 集群部署方式
            Set<String> clusterNodes = StringUtils.commaDelimitedListToSet(redisCacheProperties.getNodes());
            RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(clusterNodes);
            lettuceConnectionFactory = new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
        }
        lettuceConnectionFactory.afterPropertiesSet();
        return new RedisCacheClient(lettuceConnectionFactory);
    }

    private LettuceClientConfiguration buildLettuceClientConfig(int poolMinIdle,
                                                                int poolMaxIdle,
                                                                long poolMaxWaitMillis,
                                                                long commandTimeoutMillis) {
        // 创建一个lettuce配置对象
        GenericObjectPoolConfig<?> redisPoolConfig = new GenericObjectPoolConfig<>();
        // 为该配置对象设置相应的必要参数
        redisPoolConfig.setMinIdle(poolMinIdle);
        redisPoolConfig.setMaxIdle(poolMaxIdle);
        redisPoolConfig.setMaxTotal(poolMaxIdle);
        redisPoolConfig.setMaxWaitMillis(poolMaxWaitMillis);
        // 通过LettucePoolingClientConfiguration提供的构建器去构建一个lettuce连接配置对象
        return LettucePoolingClientConfiguration.builder()
                .poolConfig(redisPoolConfig)
                .commandTimeout(Duration.ofMillis(commandTimeoutMillis))
                .build();
    }

}
